<?php
/**
 * @see Alinks_Factory_Keeper_Interface
 */
require_once('Alinks/Factory/Keeper/Interface.php');

/**
 * @see Alinks_Factory_Interface
 */
require_once('Alinks/Factory/Interface.php');

/**
 * @see Alinks_Ajax_Callable_Interface
 */
require_once('Alinks/Ajax/Callable/Interface.php');

/**
 * Manages the installed factories
 *
 * The "gate keeper" for factories. All managing of installed factoires
 * happens through this class.
 *
 * Copyright (C) 2008  Sean Hickey
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Sean Hickey <sean@headzoo.com>
 * @version $Id: Keeper.php 388 2008-07-03 05:36:50Z sean $
 * @package Alinks_Factory
 */
class Alinks_Factory_Keeper implements Alinks_Factory_Keeper_Interface, Alinks_Ajax_Callable_Interface
{
	/**
	 * The installed factories
	 *
	 * @var Alinks_Factory_Interface[]
	 */
	protected $_factories = null;
	
	/**
	 * The factories that are incompatible with this version
	 * of aLinks
	 *
	 * @var Alinks_Factory_Interface[]
	 */
	protected $_incompatible = null;
	
	/**
	 * The settings for all the factories
	 *
	 * @var array
	 */
	protected $_factory_settings = null;
	
	/**
	 * The path where factories are installed
	 *
	 * @var string
	 */
	protected $_path = null;
	
	/**
	 * The version of aLinks that is running
	 *
	 * @var string
	 */
	protected $_alinks_version = null;
	
	/**
	 * The version of WP that is running
	 *
	 * @var string
	 */
	protected $_wp_version = null;
	
	/**
	 * Instance of Alinks_Zip_Interface for installing factories
	 *
	 * @var Alinks_Zip_Interface
	 */
	protected $_zipper;
	
	/**
	 * Whether the next method call is coming from an Ajax
	 * request
	 *
	 * @var bool
	 */
	protected $_ajax_calling = false;
	
	/**
	 * Constructor
	 *
	 * @param string $install_path The directory where factories are installed
	 * @param array $settings The factory settings
	 * @param string $alinks_version The version of aLinks that's running
	 * @param string $wp_version The version of WP that's running
	 */
	public function __construct($install_path, array $settings, $alinks_version, $wp_version)
	{
		$this->setInstallPath($install_path);
		$this->setFactorySettings($settings);
		$this->setAlinksVersion($alinks_version);
		$this->setWpVersion($wp_version);
	}
	
	/**
	 * Sets the directory where factories are installed
	 *
	 * @param string $install_path The directory
	 * @return Alinks_Factory_Keeper
	 */
	public function setInstallPath($install_path)
	{
		if (!file_exists($install_path)) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("The factories path '$install_path' does not exist");
		}
		
		$this->_path = rtrim($install_path, '/\\');
		return $this;
	}
	
	/**
	 * Sets the settings for all the factories
	 *
	 * @param array $settings The factory settings
	 * @return Alinks_Factory_Keeper
	 */
	public function setFactorySettings(array $settings)
	{
		$this->_factory_settings = $settings;
		return $this;
	}
	
	/**
	 * Sets the version of aLinks that is running
	 *
	 * @param string $version The aLinks version
	 * @return Alinks_Factory_Keeper
	 */
	public function setAlinksVersion($version)
	{
		$this->_alinks_version = $version;
		return $this;
	}
	
	/**
	 * Sets the version of WP that is running
	 *
	 * @param string $version The WP version
	 * @return Alinks_Factory_Keeper
	 */
	public function setWpVersion($version)
	{
		$this->_wp_version = $version;
		return $this;
	}
	
	/**
	 * Sets an instance of Alinks_Zip_Interface
	 *
	 * Sets the object that will be used to unzip uploaded factory zip
	 * files.
	 *
	 * @param Alinks_Zip_Interface $zipper The object
	 * @return Alinks_Factory_Keeper
	 */
	public function setZipper(Alinks_Zip_Interface $zipper)
	{
		$this->_zipper = $zipper;
		return $this;
	}
	
	/**
	 * Returns the set install path
	 *
	 * @return string
	 */
	public function getInstallPath()
	{
		return $this->_path;
	}
	
	/**
	 * Returns all the factory settings
	 *
	 * @return array
	 */
	public function getFactorySettings()
	{
		return $this->_factory_settings;
	}
	
	/**
	 * Returns the version of aLinks that is running
	 *
	 * Returns the value set with setAlinksVersion().
	 *
	 * @return string
	 */
	public function getAlinksVersion()
	{
		return $this->_alinks_version;
	}
	
	/**
	 * Returns the version of WP that is running
	 *
	 * @return string
	 */
	public function getWpVersion()
	{
		return $this->_wp_version;
	}
	
	/**
	 * Returns the set instance of Alinks_Zip_Interface
	 *
	 * @return Alinks_Zip_Interface
	 */
	public function getZipper()
	{
		return $this->_zipper;
	}
	
	/**
	 * Returns the installed factories
	 *
	 * If $safe_name is specified, then only the factory with that safe
	 * name will be returned.
	 *
	 * @param string $safe_name Factory safe name
	 * @return Alinks_Factory_Interface[]
	 */
	public function fetchAll($safe_name = null)
	{
		if (null === $this->_factories) {
			$this->_verifySetup();
			
			$this->_factories = array();
			$this->_incompatible = array();
			
			$iterator = new DirectoryIterator($this->_path);
			foreach($iterator as $dir) {
				if ($dir->isDot() || $dir->isFile() || substr($dir->getFilename(), 0 , 1) == '.') {
					continue;
				}
				
				try {
					$this->_verifyFactory($dir->getFilename());
				} catch (Alinks_Factory_Keeper_Exception $e) {
					continue;
				}
				
				$class = $this->_getClassName($dir->getFilename());
				$class = new $class();
				if (!$class instanceof Alinks_Factory_Interface) {
					/**
					 * @see Alinks_Factory_Keeper_Exception
					 */
					require_once('Alinks/Factory/Keeper/Exception.php');
					throw new Alinks_Factory_Keeper_Exception("The class $class must implement Alinks_Factory_Interface");
				}
				if (!$this->_isCompatible($class->getAlinksVersion()) || !$this->_isCompatibleWp($class->getWpVersion())) {
					$this->_incompatible[$class->getSafeName()] = $class;
					continue;
				}
				
				if (isset($this->_factory_settings[$class->getSafeName()])) {
					$class->setSettings($this->_factory_settings[$class->getSafeName()]);
				}
				
				$class->init();
				$this->_factories[$class->getSafeName()] = $class;
			}
		}

		/**
		 * If no factory name was specified, then return them all
		 */
		if (null === $safe_name) {
			return $this->_factories;
		}
		
		if (!isset($this->_factories[$safe_name])) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("The factory $safe_name does not exist");
		}
		
		return $this->_factories[$safe_name];
	}
	
	/**
	 * Returns the factories that are incompatible with this
	 * version of aLinks
	 *
	 * @return Alinks_Factory_Interface[]
	 */
	public function fetchIncompatible()
	{
		if (null === $this->_factories) {
			$this->getFactories();
		}
		
		return $this->_incompatible;
	}
	
	/**
	 * Installs a factory
	 *
	 * Given the path to an uploaded ZIP file, this method installs the
	 * factory contained in the file.
	 *
	 * @param string $zip_file The path to the uploaded zip file
	 * @throws Alinks_Factory_Keeper_Exception
	 */
	public function install($zip_file)
	{
		if (null === $this->_zipper) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("An instance of Alinks_Zip_Interface must be set");
		}
		
		/**
		 * Get the sub directories of the factories directory so we can
		 * compare later on.
		 */
		$sub_dirs = $this->_getSubDirectories($this->_path);
		
		/**
		 * Unzip the uploaded zip file to the factories directory.
		 */
		$this->_zipper->setFile($zip_file);
		try {
			$this->_zipper->unzip($this->_path);
		} catch (Alinks_Exception $e) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("Error with ZIP file: " . $e->getMessage());
		}
		
		/**
		 * We can find out the name of the newly created sub directory by comparing
		 * the factoires directory before and after unzipping the uploaded file.
		 */
		$new_dirs = array_diff($this->_getSubDirectories($this->_path), $sub_dirs);
		if (count($new_dirs) == 0) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("There was an error installing the factory. Could not determine the factory sub directory");
		}
		
		/**
		 * The directory that exists in the factories directory that wasn't there
		 * before unzipping is the safe name of the factory.
		 */
		$safe_name = array_shift($new_dirs);
		try {
			$this->_verifyFactory($safe_name);
		} catch (Alinks_Factory_Keeper_Exception $e) {
			$this->_unlinkDeep($this->_path . "/$safe_name");
			throw $e;
		}
		
		if (null !== $this->_factories) {
			$this->_factories = null;
			$this->_incompatible = null;
		}
		
		Alinks_Registry::get('Alinks_Cacher')->flush();
	}
	
	/**
	 * Uninstalls a factory
	 *
	 * Given the safe name of the factory, this method uninstalls it by
	 * deleting it's directory.
	 *
	 * @param string $safe_name The factory safe name
	 * @return bool
	 * @throws Alinks_Factory_Keeper_Exception
	 */
	public function uninstall($safe_name)
	{
		$directory = $this->_path . "/$safe_name";
		if (!file_exists($directory)) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("The directory $directory does not exist");
		}
		
		$this->_unlinkDeep($directory);
		
		if (null !== $this->_factories) {
			$this->_factories = null;
			$this->_incompatible = null;
		}
	}
	
	/**
	 * Returns the form for a given factory
	 *
	 * @param string $safe_name The factory safe name
	 * @return Alinks_Form
	 */
	public function getFactoryKeyphrasesForm($safe_name)
	{
		$factory = $this->fetchAll($safe_name);
		if (!$factory->hasKeyphrasesForm()) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("The factory $safe_name does not have a keyphrases form");
		}
		
		$form = $factory->getKeyphrasesForm();
		$factory_element = new Alinks_Form_Element_Hidden('factory');
		$factory_element->setValue($safe_name);
		$form->addElement($factory_element);
		
		if ($this->_ajax_calling) {
			/**
			 * @see Alinks_Form_Json
			 */
			require_once('Alinks/Form/Json.php');
			$form = new Alinks_Form_Json($form);
			Alinks_Ajax::sendResponse(true, null, array(
				'form' => $form->getJson()
			));
		}
		
		return $form;
	}
	
	/**
	 * Implements Alinks_Ajax_Callable_Interface::setAjaxCalling
	 */
	public function setAjaxCalling()
	{
		$this->_ajax_calling = true;
	}
	
	/**
	 * Returns the name of a factory class for a safe name
	 *
	 * Given the safe name of the factory, this method returns the class
	 * name for the factory.
	 *
	 * @param string $safe_name The factory safe name
	 * @return string
	 */
	protected function _getClassName($safe_name)
	{
		return 'Alinks_Factory_' . $this->_camelCase($safe_name);
	}
	
	/**
	 * Verifies that a factory is installed correctly
	 *
	 * Checks that the factory sub directory contains the factory.php script, and
	 * that the factory.php script contains a class with the correct factory
	 * name.
	 *
	 * @param string $safe_name The factory safe name
	 * @throws Alinks_Factory_Keeper_Exception
	 */
	protected function _verifyFactory($safe_name)
	{
		$factory_file = $this->_path . "/$safe_name/factory.php";
		if (!file_exists($factory_file)) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("Could not find the file $factory_file");
		}
		require_once($factory_file);
		$class = $this->_getClassName($safe_name);
		if (!class_exists($class)) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("Could not find class. The file $file should contain a class with the name $class");
		}
	}
	
	/**
	 * Delets a directory and all it's sub directories and files
	 *
	 * @param string $path The directory to delete
	 * @throws Alinks_Factory_Keeper_Exception
	 */
	protected function _unlinkDeep($path)
	{
		$iterator = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator($path),
			RecursiveIteratorIterator::CHILD_FIRST
		);

		foreach($iterator as $file) {
			if ($file->isFile()) {
				$result = @unlink($file->getPathname());
			} else {
				$result = @rmdir($file->getPathname());
			}
			if (false === $result) {
				/**
				 * @see Alinks_Factory_Keeper_Exception
				 */
				require_once('Alinks/Factory/Keeper/Exception.php');
				throw new Alinks_Factory_Keeper_Exception("Could not delete the directory '" . $file->getPathname() . "'");
			}
		}
		
		/**
		 * If the iterator isn't unset, the directory is left open, and
		 * the next call to rmdir() will fail.
		 */
		unset($iterator);
		
		if (@rmdir($path) === false) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("Could not delete the directory '$path'");
		}
	}
	
	/**
	 * Converts an underscored string into a camel cased string
	 *
	 * @param string $string The string to change
	 * @return string
	 */
	protected function _camelCase($string)
	{
		$string = str_replace('_', ' ', $string);
		$string = ucwords(strtolower($string));
		$string = str_replace(' ', '', $string);
		return $string;
	}
	
	/**
	 * Returns if a factory is compatible with this version of aLinks
	 *
	 * @param string $version The version string
	 * @return bool
	 */
	protected function _isCompatible($version)
	{
		$version = str_replace('.', '\\.', $version);
		$version = str_replace('*', '.*?', $version);
		
		return preg_match('/^' . $version . '$/', $this->_alinks_version);
	}
	
	/**
	 * Returns if the factory is compatible with this version of WordPress
	 *
	 * @param string $version The version string
	 * @return bool
	 */
	protected function _isCompatibleWp($version)
	{
		$version = str_replace('.', '\\.', $version);
		$version = str_replace('*', '.*?', $version);
		
		return preg_match('/^' . $version . '$/', $this->_wp_version);
	}
	
	/**
	 * Verifies that all need properties are set
	 *
	 * Checks that required properties are set before this class can try
	 * to load installed factories.
	 *
	 * @throws Alinks_Factory_Keeper_Exception
	 */
	protected function _verifySetup()
	{
		if (null === $this->_path) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("The factories installation path must be set");
		}
		if (null === $this->_factory_settings) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("The factory settings must be set");
		}
		if (null === $this->_alinks_version) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("The aLinks version must be set");
		}
	}
	
	/**
	 * Returns an array of the sub directories found in $path
	 *
	 * @param string $path The top level directory
	 * @return array
	 * @throws Alinks_Factory_Keeper_Exception
	 */
	protected function _getSubDirectories($path)
	{
		if (!file_exists($path)) {
			/**
			 * @see Alinks_Factory_Keeper_Exception
			 */
			require_once('Alinks/Factory/Keeper/Exception.php');
			throw new Alinks_Factory_Keeper_Exception("The directory $path does not exist");
		}
		
		$dirs = array();
		$iterator = new DirectoryIterator($path);
		foreach($iterator as $dir) {
			if ($dir->isDot() || $dir->isFile()) {
				continue;
			}
			$dirs[] = $dir->getFilename();
		}
		
		return $dirs;
	}
}